extends Node

signal request_node_released

const SDK_EXECUTABLE: String = "lbrynet"
const STANDALONE_SDK_LOCATION: String = "user://" + SDK_EXECUTABLE
const RPC_PROTOCOL: String = "http"
const MAX_CONNECTION_RETRIES: int = 4
const MAX_CONCURRENT_REQUESTS: int = 20

var preference_mutex := Mutex.new()
var _rpc_address: String = "127.0.0.1"
var _rpc_port: String = "5279"
var _sdk_command: String
# Pool of HTTPRequest nodes
var _request_nodes_pool: Array = []
# Boolean array of which HTTPRequest nodes are in use
var _request_nodes_claimed: Array = []


func _ready() -> void:
	for _count in range(MAX_CONCURRENT_REQUESTS):
		var request := HTTPRequest.new()
		_request_nodes_pool.append(request)
		_request_nodes_claimed.append(false)
		add_child(request)
	
	_initial_setup()
	if _find_executable():
		start()
		# Allow time for the daemon to start
		yield(get_tree().create_timer(0.5), "timeout")
		var connected: bool = yield(_check_connection(), "completed")
		var retries: int = 0
		while not connected and retries < MAX_CONNECTION_RETRIES:
			yield(get_tree().create_timer(1), "timeout")
			connected = yield(_check_connection(), "completed")
			retries += 1
		if not connected:
			_failed_to_connect_popup()


func _notification(what: int) -> void:
	if what == MainLoop.NOTIFICATION_WM_QUIT_REQUEST:
		if Data.get_stop_daemon_on_exit():
			# Stop the daemon before closing the program
			yield(stop(), "completed")
		get_tree().quit()


func start() -> void:
	""" Start the lbrynet daemon """
	OS.execute(_sdk_command, ["start"], false)


func stop() -> Dictionary:
	""" Stop the lbrynet daemon """
	var data: Dictionary = {"method": "stop", "params": {}}
	return yield(_rpc(data), "completed")


func is_ready() -> bool:
	""" Check if the daemon is ready to be queried """
	var is_up: bool = true
	var current_status: Dictionary = yield(status(), "completed")
	if (
			current_status.has("result") 
			and current_status["result"].has("startup_status")
	):
		var components_up_status = current_status["result"]["startup_status"]
		for component in components_up_status:
			if components_up_status[component] == false:
				is_up = false
				break
	else:
		is_up = false
	return is_up


func wait_until_ready() -> void:
	""" Completes when `is_ready()` returns `true` """
	var wait_timer := Timer.new()
	wait_timer.set_one_shot(true)
	add_child(wait_timer)
	var sdk_ready: bool = yield(SDK.is_ready(), "completed")
	while sdk_ready == false:
		wait_timer.start(1)
		yield(wait_timer, "timeout")
		sdk_ready = yield(SDK.is_ready(), "completed")
	remove_child(wait_timer)
	wait_timer.queue_free()


func status() -> Dictionary:
	""" Get the status of the lbrynet daemon """
	var data: Dictionary = {"method": "status", "params": {}}
	return yield(_rpc(data), "completed")


func gets(args: Dictionary) -> Dictionary:
	var data: Dictionary = {"method": "get", "params": args}
	return yield(_rpc(data), "completed")


func resolve(urls: Array) -> Dictionary:
	# Must have `yield` here so that a `GDScriptFunctionState` is returned on
	# all paths
	yield(get_tree(), "idle_frame")
	
	var response: Dictionary
	# Check the cache
	if len(urls) == 1:
		response = Cache.get_resolved_url(urls[0])
		if not response.empty():
			return response
	
	# Get the response
	var data: Dictionary = {"method": "resolve", "params": {"urls": urls}}
	response = yield(_rpc(data), "completed")
	# Add to cache
	if len(urls) == 1:
		Cache.add_resolved_url(urls[0], response)
	return response


func claim_search(args: Dictionary) -> Dictionary:
	var data: Dictionary = {"method": "claim_search", "params": args}
	return yield(_rpc(data), "completed")


func preference_get() -> Dictionary:
	var data: Dictionary = {"method": "preference_get", "params": {}}
	return yield(_rpc(data), "completed")


func preference_set(key: String, new_preference) -> void:
	"""
	When changing the preferences make sure to aquire `preference_mutex` before
	getting the preferences and release the mutex once the preferences have
	been changed
	"""
	var data: Dictionary = {
		"method": "preference_set",
		"params": {"key": key, "value": new_preference}
	}
	yield(_rpc(data), "completed")


func file_list(args: Dictionary) -> Dictionary:
	var data: Dictionary = {"method": "file_list", "params": args}
	return yield(_rpc(data), "completed")


func _rpc(data: Dictionary) -> Dictionary:
	"""
	Make an RPC to the lbrynet daemon
	See here for information: https://lbry.tech/api/sdk
	
	Args:
		data: as an example, to stop the daemon, 
			data = {"method": "stop", "params": {}}
	"""
	var response: Dictionary = {}
	var request_node_idx: int = yield(_claim_request_node(), "completed")
	var request: HTTPRequest = _request_nodes_pool[request_node_idx]
	var body = JSON.print(data)
	var err = request.request("%s://%s:%s" 
			% [RPC_PROTOCOL, _rpc_address, _rpc_port]
			, [], false, HTTPClient.METHOD_POST, body)
	if err == OK:
		response = callv(
				"_parse_rpc_response", yield(request, "request_completed"))
	else:
		# Must have a yield on each code path
		yield(get_tree(), "idle_frame")
		push_error("HTTPClient request error: %d" % err)
	_release_request_node(request_node_idx)
	return response


func _parse_rpc_response(result: int, _response_code: int, 
		_headers: PoolStringArray, body: PoolByteArray) -> Dictionary:
	""" Returns the response of the lbrynet daemon from the RPC """
	if result == HTTPRequest.RESULT_SUCCESS and _response_code == 200:
		var json_result: JSONParseResult = JSON.parse(body.get_string_from_utf8())
		if json_result.get_result() is Dictionary:
			return json_result.get_result()
	return {}


func _find_executable() -> bool:
	"""
	Check if the lbrynet executable is available, if not then tell the user
	and exit the program
	
	Returns:
		`true` if the executable was found, `false` otherwise
	"""
	var file := File.new()
	if file.file_exists(STANDALONE_SDK_LOCATION):
		_sdk_command = ProjectSettings.globalize_path(STANDALONE_SDK_LOCATION)
	else:
		_sdk_command = SDK_EXECUTABLE
	
	# Check if the `lbrynet` command is available
	var exit_code: int = OS.execute(_sdk_command, [])
	if exit_code != 0:
		_executable_not_found_popup()
	return bool(not exit_code)


func _check_connection() -> bool:
	"""
	Check if a connection can be made to the lbrynet daemon
	
	Returns:
		`true` if a connection can be made, `false` otherwise
	"""
	return bool(len(yield(status(), "completed")))


func _initial_setup() -> void:
	""" Update variables based on settings and command line arguments """
	var settings_rpc_address: PoolStringArray = LbrynetConfig.get_rpc_address()
	var cmdline_rpc_address: PoolStringArray = Cmdline.get_rpc_address()
	if len(settings_rpc_address) > 0:
		_rpc_address = settings_rpc_address[0]
		if len(settings_rpc_address) > 1:
			_rpc_port = settings_rpc_address[1]
	if len(cmdline_rpc_address) > 0:
		_rpc_address = cmdline_rpc_address[0]
		if len(cmdline_rpc_address) > 1:
			_rpc_port = cmdline_rpc_address[1]


func _executable_not_found_popup() -> void:
	var msg: String = "'lbrynet' executable not found"
	var popup_msg: String = msg \
			+ ".\n\nPlease download the lbrynet executable and place" \
			+ " it in '%s' or add its" \
			+ " location to your PATH environment variable." \
			+ "\n\nDownload link:" \
			+ " https://github.com/lbryio/lbry-sdk/releases"
	popup_msg = popup_msg % OS.get_user_data_dir()
	var popup := AcceptDialog.new()
	popup.set_exclusive(true)
	popup.set_autowrap(true)
	popup.set_text(popup_msg)
	get_tree().get_root().get_children()[-1].add_child(popup)
	popup.popup_centered_clamped(Vector2(500, 50), 0.75)
	push_error(msg)
	yield(popup, "confirmed")
	get_tree().notification(MainLoop.NOTIFICATION_WM_QUIT_REQUEST)


func _failed_to_connect_popup() -> void:
	var msg: String = "Failed to connect to the lbrynet daemon"
	var popup_msg: String = msg + ".\n\nHave you changed the RPC port?"
	var popup := AcceptDialog.new()
	popup.set_exclusive(true)
	popup.set_autowrap(true)
	popup.set_text(popup_msg)
	get_tree().get_root().get_children()[-1].add_child(popup)
	popup.popup_centered_clamped(Vector2(500, 50), 0.75)
	push_error(msg)
	yield(popup, "confirmed")
	get_tree().notification(MainLoop.NOTIFICATION_WM_QUIT_REQUEST)


func _claim_request_node() -> int:
	"""
	Waits for a HTTPRequest node to become available, then marks it as in use.
	When finished using it should call `_release_request_node()` to mark it as
	free
	
	Returns:
		The index in `_request_nodes_pool` of the claimed request node
	"""
	yield(get_tree(), "idle_frame")
	var idx: int = -1
	for i in range(len(_request_nodes_claimed)):
		if _request_nodes_claimed[i] == false:
			_request_nodes_claimed[i] = true
			idx = i
			break
	while idx < 0:
		yield(self, "request_node_released")
		for i in range(len(_request_nodes_claimed)):
			if _request_nodes_claimed[i] == false:
				_request_nodes_claimed[i] = true
				idx = i
				break
	return idx


func _release_request_node(idx: int) -> void:
	"""
	Marks the HTTPRequest node as free to use
	
	Args:
		idx: index of the HTTPRequest node in `_request_nodes_pool`
	"""
	_request_nodes_claimed[idx] = false
	emit_signal("request_node_released")
